package de.mmis.core.publishsubscribe;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.log4j.Logger;

import de.mmis.core.base.DeserializableAnnotations.DeserializationConstructor;
import de.mmis.core.base.MethodDescriptor;
import de.mmis.core.base.MethodDescriptor.P;
import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.event.Event;

/**
 * provides methods for de/serializing events
 * 
 * @author Martin Nyolt
 * 
 */
public class EventHelper {

	private final static Logger LOGGER = Logger.getLogger(EventHelper.class);

	public static class EventDataMap extends HashMap<String, Object> {
		private static final long serialVersionUID = -3989893955575291823L;

		public EventDataMap() {
			// nothing
		}

		public EventDataMap(Map<? extends String, ?> map) {
			super(map);
		}
	}

	private EventHelper() {
		// hide constructor
	}

	public static boolean hasDeserializationConstructor(Class<?> clazz) {
		for (Constructor<?> cons : clazz.getConstructors()) {
			if (cons.getAnnotation(DeserializationConstructor.class) != null) {
				return true;
			}
		}

		return false;
	}

	public static EventDataMap getEventData(Event event) {
		// assign the result to eventData after it's filled, to be thread safe
		EventDataMap result = new EventDataMap();

		ObjectDescriptor descr = new ObjectDescriptor(event, false);
		for (MethodDescriptor m : descr.getGetterMethodDescriptors()) {
			String key = m.getGetterName();
			if (key == null)
				continue;

			if (key.isEmpty()) {
				LOGGER.error("Key name must not be empty - ignoring");
				continue;
			}

			try {
				result.put(key, m.invoke());
			} catch (IllegalArgumentException e) {
				LOGGER.error("Could not invoke getter method, ignoring", e);
				continue;
			} catch (IllegalAccessException e) {
				LOGGER.error("Could not invoke getter method, ignoring", e);
				continue;
			} catch (InvocationTargetException e) {
				LOGGER.error("Could not invoke getter method, ignoring", e);
				continue;
			}
		}

		return result;
	}

	/**
	 * Converts an {@link Event} to a {@link PlainEvent} event, also adding
	 * meta-data like <code>:sender</code> and <code>:time</code>
	 * 
	 * @param event
	 * @param id
	 * @return
	 */
	static PlainEvent event2plain(Event event, String sender) {
		Set<String> classes = new HashSet<String>();

		Class<?> baseClass = event.getClass();
		for (Class<?> clazz = baseClass; clazz != null; clazz = clazz
				.getSuperclass()) {
			// exclude java.lang.object
			if (Object.class.equals(clazz))
				continue;

			// don't add abstract classes
			// the subscribe-Method does not accept these, so we never want to
			// get an abstract class
			if (!Modifier.isAbstract(clazz.getModifiers()))
				classes.add(clazz.getName());

			LinkedList<Class<?>> interfaces = new LinkedList<Class<?>>(
					Arrays.asList(clazz.getInterfaces()));

			while (!interfaces.isEmpty()) {
				Class<?> i = interfaces.removeFirst();
				classes.add(i.getName());
				interfaces.addAll(Arrays.asList(i.getInterfaces()));
			}
		}

		EventDataMap eventData = new EventDataMap(getEventData(event));
		Map<String, Tree> plainEventData = new HashMap<String, Tree>();

		for (Entry<String, Object> entry : eventData.entrySet()) {
			String key = entry.getKey();
			// always serialize implicit
			Tree value = Tree.fromObject(entry.getValue(), false);
			plainEventData.put(key, value);
		}

		plainEventData.put(":time",
				Tree.fromObject(new Long(System.currentTimeMillis())));
		plainEventData.put(":sender", Tree.fromObject(sender));

		Tree serialized = null;
		if (!hasDeserializationConstructor(baseClass))
			serialized = Tree.fromObject(event);

		return new PlainEvent(baseClass.getName(), classes, plainEventData,
				serialized);
	}

	/**
	 * <p>
	 * returns an Event of class T. If the class is abstract or the Event could
	 * not be generated, <code>null</code> is returned.
	 * </p>
	 * <p>
	 * If T denotes an Interface, a {@link Proxy} implementing that interface is
	 * created.
	 * </p>
	 * <p>
	 * If T denotes a non-abstract class, a constructor of that class will be
	 * called to create this event. The constructor is choosen by
	 * </p>
	 * 
	 * @param <T>
	 *            The type of the Event clazz
	 * @param event
	 *            The {@link PlainEvent}
	 * @param clazz
	 *            The {@link Class} object of T
	 */
	public static <T extends Event> T plain2event(PlainEvent event,
			Class<T> clazz) {
		if (event.serializedEvent != null)
			try {
				return event.serializedEvent.getAs(clazz);
			} catch (DeserializableException e) {
				LOGGER.error(
						"Deserialization of event from original serialized event failed - trying creating new event",
						e);
			}

		int mod = clazz.getModifiers();
		if (Modifier.isAbstract(mod) && !Modifier.isInterface(mod)) {
			LOGGER.error("Can't instantiate the abstact class " + clazz);
			return null;
		}

		Map<String, Tree> eventData = event.eventData;

		if (clazz.isInterface())
			return plain2event_interface(eventData, clazz);

		return plain2event_class(eventData, clazz);
	}

	private static <T extends Event> T plain2event_class(
			Map<String, Tree> eventData, Class<T> clazz) {
		Constructor<T> cons = null;
		// how many parameter cons has
		int matchCount = 0;

		consLoop: for (Constructor<?> con : clazz.getConstructors()) {
			@SuppressWarnings("unchecked")
			Constructor<T> c = (Constructor<T>) con;

			if (c.getAnnotation(DeserializationConstructor.class) == null)
				continue;

			HashSet<String> ps = new HashSet<String>();
			int parCount = 0;
			for (Annotation[] as : c.getParameterAnnotations()) {
				parCount++;
				boolean hadP = false;

				for (Annotation a : as) {
					if (a instanceof P) {
						hadP = true;
						ps.add(((P) a).value());
					}
				}

				if (!hadP) {
					LOGGER.warn("parameter " + parCount
							+ " of constructor is not annotated with "
							+ P.class + ": " + c);

					// the parameter had not the P annotation, ignore the
					// constructor
					continue consLoop;
				}
			}

			// if ps is a subset of eventData.keySet, all parameters can be
			// initialized
			if (eventData.keySet().containsAll(ps))
				if (ps.size() > matchCount) {
					matchCount = ps.size();
					cons = c;
				}
		}

		if (cons == null) {
			LOGGER.error("No matching constructor found");
			return null;
		}

		Class<?>[] paraTypes = cons.getParameterTypes();
		Object[] parameters = new Object[paraTypes.length];
		int i = 0;
		for (Annotation[] as : cons.getParameterAnnotations()) {
			for (Annotation a : as) {
				if (!(a instanceof P))
					continue;

				String key = ((P) a).value();
				Tree value = eventData.get(key);
				Class<?> p_class = paraTypes[i];
				try {
					parameters[i] = value.getAs(p_class);
				} catch (DeserializableException e) {
					LOGGER.error("Could not deserialize event data", e);
				}
			}
			i++;
		}

		try {
			return cons.newInstance(parameters);
		} catch (IllegalArgumentException e) {
			LOGGER.error("Could not create event of class " + clazz, e);
			return null;
		} catch (InstantiationException e) {
			LOGGER.error("Could not create event of class " + clazz, e);
			return null;
		} catch (IllegalAccessException e) {
			LOGGER.error("Could not create event of class " + clazz, e);
			return null;
		} catch (InvocationTargetException e) {
			LOGGER.error("Could not create event of class " + clazz, e);
			return null;
		}

	}

	private static class EventInvocationHandler implements InvocationHandler {

		@SuppressWarnings("hiding")
		private final static Logger LOGGER = Logger
				.getLogger(EventInvocationHandler.class);

		private final Map<String, Tree> eventData;
		private final EventDataMap results = new EventDataMap();
		private final Class<?> clazz;

		public EventInvocationHandler(Map<String, Tree> eventData,
				Class<?> clazz) {
			this.eventData = eventData;
			this.clazz = clazz;
		}

		private void buildResults() {
			for (Entry<String, Tree> entry : eventData.entrySet()) {
				String key = entry.getKey();
				if (key.startsWith(":") || results.containsKey(key))
					continue;

				// search for a method
				Method m = null;
				try {
					m = clazz.getMethod("get" + key);
				} catch (SecurityException e) {
					// handle below
				} catch (NoSuchMethodException e) {
					// handle below
				}

				if (m == null)
					try {
						m = clazz.getMethod("is" + key);
					} catch (SecurityException e) {
						// handle below
					} catch (NoSuchMethodException e) {
						// handle below
					}

				if (m == null) {
					LOGGER.warn("No corresponding method for key " + key
							+ " found in class " + clazz);
					continue;
				}

				try {
					results.put(key, entry.getValue().getAs(m.getReturnType()));
				} catch (DeserializableException e) {
					LOGGER.error("Could not deserialize value", e);
				}
			}
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			String key = null;

			String name = method.getName();
			if (name.startsWith("is"))
				key = name.substring(2);
			else if (name.startsWith("get"))
				key = name.substring(3);

			if (key == null) {
				throw new UnsupportedOperationException("The method " + method
						+ " is not a getter");
			}

			if (results.containsKey(key))
				return results.get(key);

			if (key.equals("EventData")) {
				buildResults();
				return results;
			}

			Tree exp = eventData.get(key);
			Class<?> retClass = method.getReturnType();
			Object result = exp.getAs(retClass);

			results.put(key, result);
			return result;
		}
	}

	private static <T extends Event> T plain2event_interface(
			Map<String, Tree> eventData, Class<T> clazz) {
		InvocationHandler ih = new EventInvocationHandler(eventData, clazz);

		@SuppressWarnings("unchecked")
		T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(),
				new Class<?>[] { clazz }, ih);

		return result;
	}
}
